package cn.luowb.checkchat.service.impl;

import cn.luowb.checkchat.common.context.UserContext;
import cn.luowb.checkchat.common.result.Result;
import cn.luowb.checkchat.common.util.RedisKeyUtil;
import cn.luowb.checkchat.common.web.Results;
import cn.luowb.checkchat.dao.entity.CommentDO;
import cn.luowb.checkchat.dao.entity.LikeDO;
import cn.luowb.checkchat.dao.entity.PostDO;
import cn.luowb.checkchat.dao.entity.UserDO;
import cn.luowb.checkchat.dao.mapper.CommentMapper;
import cn.luowb.checkchat.dao.mapper.LikeMapper;
import cn.luowb.checkchat.dao.mapper.PostMapper;
import cn.luowb.checkchat.dao.mapper.UserMapper;
import cn.luowb.checkchat.dto.resp.*;
import cn.luowb.checkchat.service.CommentService;
import cn.luowb.checkchat.service.LikeService;
import cn.luowb.checkchat.service.PostService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author heimenkyou
 * @description 针对表【like(点赞)】的数据库操作Service实现
 * @createDate 2025-09-01 19:07:09
 */
@Service
@Slf4j
public class LikeServiceImpl extends ServiceImpl<LikeMapper, LikeDO> implements LikeService {
    @Resource
    private LikeMapper likeMapper;
    @Resource
    private PostService postService;
    @Resource
    private PostMapper postMapper;
    @Resource
    private CommentService commentService;
    @Resource
    private CommentMapper commentMapper;
    @Resource
    private UserMapper userMapper;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedisKeyUtil redisKeyUtil;

    private static final int TYPE_POST = 0;
    private static final int TYPE_COMMENT = 1;


    /**
     * 点赞切换：点一下即点赞；再点一下即取消。
     * 实现策略：
     * 1）以 MySQL 唯一键 (uid,type,obj_id) 为最终“已赞/未赞”的权威判定；
     * 2）DB 成功后写穿 Redis：
     *    - Set：like:set:{type}:{objId} 成员是 uid（可用 SISMEMBER 快速判断“我是否点赞”）
     * 3）post/comment 表上的 like_count 作为直读排序字段（DB 内部 +1/-1，防止负数）
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result<ToggleLikeVO> togglelike(int type, long objId) {
        // ========= 0. 基本参数校验 =========
        if (objId < 0 || (type != TYPE_POST && type != TYPE_COMMENT)) {
            throw new RuntimeException("参数错误");
        }

        // ========= 1. 当前登录用户 =========
        Long uid = UserContext.getUserId();
        if (uid == null || uid < 0) {
            throw new RuntimeException("用户未登录");
        }

        // ========= 2. 目标存在性校验（可按需关闭以减轻查询负载）=========
//        boolean targetExists = (type == TYPE_POST)
//                ? postMapper.existsById(objId) > 0
//                : commentMapper.existsById(objId) > 0;
//        if (!targetExists) {
//            throw new RuntimeException("目标不存在或已删除");
//        }

        // Redis Key 约定
        final String setKey = redisKeyUtil.buildLikeSetKey(type, objId);

        // ========= 3. MySQL 事务内切换逻辑 =========
        boolean likedNow;     // 切换后是否为“已点赞”态

        try {
            // 3.1 优先尝试“插入”点赞记录：
            //     若触发唯一键冲突 => 说明之前点过赞 => 改为“取消点赞”
            LikeDO row = new LikeDO();
            row.setUid(uid);
            row.setType(type);
            row.setObjId(objId);

            try {
                likeMapper.insert(row); // 唯一键 (uid,type,obj_id) 若已存在将抛 DuplicateKeyException
                // —— 插入成功：本次为“点赞”
                likedNow = true;

                // 3.2 对应对象 like_count +1（避免负数、并发条件下由 DB 负责正确性）
                if (type == TYPE_POST) {
                    postService.update()
                            .eq("id", objId)
                            .setSql("like_count = like_count + 1")
                            .update();
                } else {
                    commentService.update()
                            .eq("id", objId)
                            .setSql("like_count = like_count + 1")
                            .update();
                }

            } catch (DuplicateKeyException alreadyLiked) {
                // —— 唯一键冲突：说明之前就有记录，本次应执行“取消点赞”
                int del = likeMapper.delete(new LambdaQueryWrapper<LikeDO>()
                        .eq(LikeDO::getUid, uid)
                        .eq(LikeDO::getType, type)
                        .eq(LikeDO::getObjId, objId));
                if (del > 0) {
                    likedNow = false;
                    if (type == TYPE_POST) {
                        postService.update()
                                .eq("id", objId)
                                .setSql("like_count = like_count - 1")
                                .update();
                    } else {
                        commentService.update()
                                .eq("id", objId)
                                .setSql("like_count = like_count - 1")
                                .update();
                    }
                } else {
                    // 极端并发表现：并未删除到记录（被其他线程抢先删除），此时按“未点赞”处理
                    likedNow = false;
                }
            }

        } catch (Exception e) {
            log.error("事务失败");
            // 事务内异常会回滚 DB
            throw new RuntimeException("服务器繁忙，请稍后再试");
        }

        // ========= 4. Redis 写穿（放在 DB 成功之后；失败不回滚 DB，仅记录日志并等待校准）=========
        try {
            if (likedNow) {
                stringRedisTemplate.opsForSet().add(setKey, String.valueOf(uid));
            } else {
                // 取消点赞：移出 Set，并把计数 -1（注意不让其小于 0）
                stringRedisTemplate.opsForSet().remove(setKey, String.valueOf(uid));
            }
        } catch (Exception re) {
            log.warn("写穿失败, 等待校准");
        }

        // 4) 统计点赞数：优先 SCARD，异常则读 DB.like_count
        int likeCountNow;
        try {
            Long sz = stringRedisTemplate.opsForSet().size(setKey);
            if (sz != null) {
                likeCountNow = sz.intValue();
            } else {
                likeCountNow = readLikeCountFromDb(type, objId);
            }
        } catch (Exception e) {
            likeCountNow = readLikeCountFromDb(type, objId);
        }

        ToggleLikeVO vo = ToggleLikeVO.builder()
                .liked(likedNow)
                .likeCount(likeCountNow)
                .type(type)
                .objId(objId)
                .build();
        return Results.success(vo);
    }

    /** 兜底从 DB 的 like_count 读取 */
    private int readLikeCountFromDb(int type, long objId) {
        Integer cnt = (type == TYPE_POST)
                ? Optional.ofNullable(postService.getById(objId)).map(PostDO::getLikeCount).orElse(0)
                : Optional.ofNullable(commentService.getById(objId)).map(CommentDO::getLikeCount).orElse(0);
        return Math.max(0, cnt == null ? 0 : cnt);
    }

    /**
     * 批量获取点赞数（使用 SCARD；Redis 异常时回退 DB）
     */
    @Override
    public Result<LikeCountsVO> getLikeCounts(int type, List<String> idsRaw) {
        if (type != TYPE_POST && type != TYPE_COMMENT) {
            throw new IllegalArgumentException("参数不合法");
        }
        List<Long> ids = idsRaw.stream().map(Long::parseLong).collect(Collectors.toList());
        if (ids.isEmpty()) {
            throw new IllegalArgumentException("参数不合法");
        }

        final int MAX_SIZE = 200;
        if (ids.size() > MAX_SIZE) {
            ids = ids.subList(0, MAX_SIZE);
        }

        // 去重且保持顺序
        LinkedHashSet<Long> orderedDistinct = ids.stream()
                .filter(Objects::nonNull)
                .filter(id -> id > 0)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        // 1) 使用 pipeline SCARD 批量统计
        List<String> setKeys = orderedDistinct.stream()
                .map(id -> redisKeyUtil.buildLikeSetKey(type, id))
                .collect(Collectors.toList());

        List<Object> scardRes = null;
        try {
            scardRes = stringRedisTemplate.executePipelined((RedisCallback<Object>) connection -> {
                var ser = stringRedisTemplate.getStringSerializer();
                for (String k : setKeys) {
                    connection.sCard(ser.serialize(k));
                }
                return null;
            });
        } catch (DataAccessException e) {
            log.warn("Redis pipeline SCARD failed, downgrade to DB");
        }

        // 2) 组装结果 & 收集需要 DB 兜底的 id
        Iterator<Long> idIt = orderedDistinct.iterator();
        Map<Long, Integer> resultMap = new LinkedHashMap<>(orderedDistinct.size());
        List<Long> missIds = new ArrayList<>();

        if (scardRes != null && scardRes.size() == setKeys.size()) {
            for (Object o : scardRes) {
                Long id = idIt.next();
                Integer v = (o instanceof Number) ? ((Number) o).intValue() : null;
                if (v != null && v >= 0) {
                    resultMap.put(id, v);
                } else {
                    missIds.add(id);
                }
            }
        } else {
            missIds.addAll(orderedDistinct);
        }

        // 3) DB 兜底（仅对 miss 的 id）
        if (!missIds.isEmpty()) {
            Map<Long, Integer> dbMap = (type == TYPE_POST)
                    ? loadPostCountsFromDb(missIds)
                    : loadCommentCountsFromDb(missIds);
            for (Long id : missIds) {
                resultMap.put(id, Math.max(0, dbMap.getOrDefault(id, 0)));
            }
        }

        // 4) 输出顺序与请求一致
        LinkedHashMap<String, Integer> ordered = new LinkedHashMap<>();
        for (Long id : ids) {
            ordered.put(String.valueOf(id), Math.max(0, resultMap.getOrDefault(id, 0)));
        }

        LikeCountsVO vo = new LikeCountsVO(type, ordered);
        return Results.success(vo);
    }

    /** 批量从 DB 读取帖子 like_count，返回 Map<id, count> */
    private Map<Long, Integer> loadPostCountsFromDb(List<Long> ids) {
        List<PostDO> list = postMapper.selectList(new LambdaQueryWrapper<PostDO>()
                .select(PostDO::getId, PostDO::getLikeCount)
                .in(PostDO::getId, ids));
        Map<Long, Integer> map = new HashMap<>(ids.size());
        if (list != null) {
            for (PostDO p : list) {
                if (p.getId() != null) {
                    map.put(p.getId(), safeNonNegative(p.getLikeCount()));
                }
            }
        }
        for (Long id : ids) map.putIfAbsent(id, 0);
        return map;
    }

    /** 批量从 DB 读取评论 like_count，返回 Map<id, count> */
    private Map<Long, Integer> loadCommentCountsFromDb(List<Long> ids) {
        List<CommentDO> list = commentMapper.selectList(new LambdaQueryWrapper<CommentDO>()
                .select(CommentDO::getId, CommentDO::getLikeCount)
                .in(CommentDO::getId, ids));
        Map<Long, Integer> map = new HashMap<>(ids.size());
        if (list != null) {
            for (CommentDO c : list) {
                if (c.getId() != null) {
                    map.put(c.getId(), safeNonNegative(c.getLikeCount()));
                }
            }
        }
        for (Long id : ids) map.putIfAbsent(id, 0);
        return map;
    }

    /** 防御性处理，避免负数 */
    private static int safeNonNegative(Integer v) {
        if (v == null) return 0;
        return Math.max(0, v);
    }

    /* ================= “我的点赞列表” 保持不变 ================= */

    @Override
    public MyLikesVO listMyLikes(Long uid, int type, int page, int pageSize) {
        Page<LikeDO> mpPage = new Page<>(page, pageSize);
        LambdaQueryWrapper<LikeDO> qw = new LambdaQueryWrapper<LikeDO>()
                .eq(LikeDO::getUid, uid)
                .eq(LikeDO::getType, type)
                .orderByDesc(LikeDO::getCreateTime);
        Page<LikeDO> likePage = this.baseMapper.selectPage(mpPage, qw);

        List<LikeDO> records = likePage.getRecords();
        if (CollectionUtils.isEmpty(records)) {
            return MyLikesVO.builder()
                    .type(type)
                    .page((int) likePage.getCurrent())
                    .pageSize((int) likePage.getSize())
                    .total(likePage.getTotal())
                    .items(Collections.emptyList())
                    .build();
        }

        List<Long> objIds = records.stream().map(LikeDO::getObjId).collect(Collectors.toList());

        Map<Long, PostDO> postMap = null;
        Map<Long, CommentDO> commentMap = null;
        Map<Long, PostDO> postForCommentMap = null;
        Map<Long, UserDO> authorMap;

        if (type == TYPE_POST) {
            List<PostDO> posts = postMapper.selectList(new LambdaQueryWrapper<PostDO>().in(PostDO::getId, objIds));
            postMap = safeIndexById(posts);
            Set<Long> authorIds = posts.stream().map(PostDO::getUid).filter(Objects::nonNull).collect(Collectors.toSet());
            authorMap = loadUsers(authorIds);
        } else {
            List<CommentDO> comments = commentMapper.selectList(new LambdaQueryWrapper<CommentDO>().in(CommentDO::getId, objIds));
            commentMap = safeIndexByIdC(comments);
            Set<Long> authorIds = comments.stream().map(CommentDO::getUid).filter(Objects::nonNull).collect(Collectors.toSet());
            Set<Long> postIds = comments.stream().map(CommentDO::getPostId).filter(Objects::nonNull).collect(Collectors.toSet());
            authorMap = loadUsers(authorIds);
            List<PostDO> posts = postIds.isEmpty() ? Collections.emptyList()
                    : postMapper.selectList(new LambdaQueryWrapper<PostDO>().in(PostDO::getId, postIds));
            postForCommentMap = safeIndexById(posts);
        }

        DateTimeFormatter ISO = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
        ZoneOffset utc = ZoneOffset.UTC;

        List<Object> items = new ArrayList<>(records.size());
        for (LikeDO like : records) {
            String likedAt = like.getCreateTime() == null ? null : like.getCreateTime().toInstant(ZoneOffset.UTC).atOffset(utc).format(ISO);
            Long objId = like.getObjId();

            if (type == TYPE_POST) {
                PostDO p = postMap.get(objId);
                if (p == null) continue;
                UserDO au = authorMap.get(p.getUid());
                items.add(MyLikedPostItemVO.builder()
                        .objId(objId)
                        .likedAt(likedAt)
                        .likeCount(safeNonNeg(p.getLikeCount()))
                        .title(nvl(p.getTitle()))
                        .author(toAuthor(au))
                        .build());
            } else {
                CommentDO c = commentMap.get(objId);
                if (c == null) continue;
                UserDO au = authorMap.get(c.getUid());
                PostDO post = postForCommentMap.get(c.getPostId());

                Map<String, Object> postBrief = new LinkedHashMap<>();
                postBrief.put("id", post == null ? null : post.getId());
                postBrief.put("title", post == null ? "" : nvl(post.getTitle()));

                items.add(MyLikedCommentItemVO.builder()
                        .objId(objId)
                        .likedAt(likedAt)
                        .likeCount(safeNonNeg(c.getLikeCount()))
                        .snippet(makeSnippet(c.getContent(), 120))
                        .post(postBrief)
                        .author(toAuthor(au))
                        .build());
            }
        }

        return MyLikesVO.builder()
                .type(type)
                .page((int) likePage.getCurrent())
                .pageSize((int) likePage.getSize())
                .total(likePage.getTotal())
                .items(items)
                .build();
    }

    /* ============== 工具 ============== */

    private Map<Long, PostDO> safeIndexById(List<PostDO> list) {
        Map<Long, PostDO> map = new HashMap<>();
        if (list != null) {
            for (PostDO p : list) {
                if (p != null && p.getId() != null) {
                    map.put(p.getId(), p);
                }
            }
        }
        return map;
    }

    private Map<Long, CommentDO> safeIndexByIdC(List<CommentDO> list) {
        Map<Long, CommentDO> map = new HashMap<>();
        if (list != null) {
            for (CommentDO c : list) {
                if (c != null && c.getId() != null) {
                    map.put(c.getId(), c);
                }
            }
        }
        return map;
    }

    private Map<Long, UserDO> loadUsers(Set<Long> uids) {
        if (CollectionUtils.isEmpty(uids)) return Collections.emptyMap();
        List<UserDO> users = userMapper.selectList(new LambdaQueryWrapper<UserDO>().in(UserDO::getUid, uids));
        Map<Long, UserDO> map = new HashMap<>();
        if (users != null) {
            for (UserDO u : users) {
                Long key = u.getUid();
                if (key != null) map.put(key, u);
            }
        }
        return map;
    }

    private AuthorBriefVO toAuthor(UserDO u) {
        if (u == null) return new AuthorBriefVO(null, "", "");
        Long key = u.getUid();
        return AuthorBriefVO.builder()
                .uid(key)
                .name(nvl(u.getLoginId()))
                .avatar(nvl(u.getAvatar()))
                .build();
    }

    private String nvl(String s) { return s == null ? "" : s; }
    private int safeNonNeg(Integer v) { return v == null ? 0 : Math.max(0, v); }
    private String makeSnippet(String content, int maxLen) {
        if (content == null) return "";
        String s = content.trim();
        if (s.length() <= maxLen) return s;
        return s.substring(0, maxLen) + "...";
    }
}